Custom actions
Model generator allows you to add custom actions to the model by passing the actionsCreators field to the createEntityModel/ createModel/createValueList methods.
Example with API interaction
To interact with an API a custom action needs a request function, which should make the API call. It also needs a few functions to reduce the state during the lifecycle of the action. One for the initial state (initial), one for a successful API response (success), and one in case the request has failed (failure).
For the initial state is is usually not necessary to change the state. Therefore _.identity (simple function that returns what it receives) is usually the right option.
However, if you need to change the initial state, here is an example for a search action. The state includes a loading status flag.
const actionCreators = {
search: {
request: ({ searchTerm }: { searchTerm: string }) => {
return api.put(`/search`, { data: { searchTerm } });
},
reduce: {
initial: (state) => {
return {
...state,
status: 'loading'
};
},
success: (state, action) => {
return {
...state,
results: action.payload,
status: 'loaded'
};
},
failure: (state, action) => {
return {
...state,
results: [],
error: action.payload,
status: 'error'
};
}
}
}
};
The same applies to the failure state, since the error is usually taken from the request itself and stored in some local state.
The success function is usually the only function that needs to be customised.
In the following example a simple status change action has been implemented. The action goToWar triggers an API call to change the status of a specific house to got-to-war. In the success reducer we simply apply the status change to the locally stored house object.
import { produce } from 'immer';
const actionCreators = {
goToWar: {
request: ({ id }: { id: string }) => {
return api.put(`/war/house/${id}`, { data: { status: 'got-to-war' } });
},
reduce: {
initial: _.identity,
success: (state: any, action: any) => {
const houseId = action.payload.data.id;
return produce(state, (newState) => {
newState.items[houseId].data.status = action.payload.data.status;
newState.items[houseId] = touchEtag(newState.items[houseId]);
});
},
failure: (state) => state
}
}
};
export const housesModel = new Generator<House, typeof actionCreators>(
'houses',
config
).createEntityModel({ actionCreators });
// go-to-war-button.tsx
function GoToWarButton({ id }: GoToWarButtonProps) {
const { goToWarWith } = useModelActions(houseModel);
return <button onClick={() => goToWar(id)}>Go to war</button>;
}
In this example produce and touchEtag are used to produce the new state and ensure all components consuming the data rerender after the action was successful.
produce
produce is a helper function from the immer library and allows us to perform the update to the state in a very simple way. It takes the object you want to mutate and a mutate function as 2 parameters. In the mutate function you can make changes that look like they would mutate the object, but produce will make it so that a new object is created. If you want to know more about produce and immer, here is a link to their docs
touchEtag
touchEtag is needed to change the etag of the changed record. The etag is used internally to keep track of changing state. If the etag remained the same, the updates to the record would not show up. If you want to dive deeper into the internal structure of model generator see the docs here.